近日,北京市设立国内首个智能网联汽车政策先行区,滴滴自动驾驶成为首批获得测试牌照的企业。同时,滴滴自动驾驶也首批获得了夜间及复杂天气场景的测试资格,可实现24小时全天候道路测试,并可在雨雾等特殊天气下开展相关测试。本篇文章来自二手程序员同学的投稿,分享了用图片来展示Git常用指令的相关内容,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章!内容全部基于 Learn Git Branching。
这是个网页版 Git 小游戏,很有意思的,希望大家可以花半天时间通关。
还有一个叫做 oh-my-git 的游戏也挺有意思。
下图展示了一个(小型)Git 代码库(本地的)。当前有两个提交记录 —— 初始提交 C0 和其后可能包含某些有用修改的提交 C1。然后,我们做一个 git commit操作,添加一个新提交,然后我们本地的仓库变为如下:可以看到的是,HEAD 拽着 main 一起指向了最新的提交 C2。图中,main 后面的 * 号,就是 HEAD。Git 的分支也非常轻量。它们只是简单地指向某个提交纪录。即使创建再多的分支也不会造成储存或内存上的开销,并且按逻辑分解工作到不同的分支要比维护那些特别臃肿的分支简单多了。只要记住使用分支其实就相当于在说:“我想基于这个提交以及它所有的父提交进行新的工作。”接下来,我们执行 git branch newImage,创建一个 newImage分支:可以看到的是,main 与 newImage 都指向了 C1。接下来,我们就可以先 checkout newImage ,然后在 newImage 上做提交了:git checkout newImage
git commit
可以看到,main 还是指向的 C1,而 newImage 指向了 C2,而且由于 checkout 的原因,HEAD 指向了 newImage。
对了,有个更简洁的方式:如果你想创建一个新的分支同时切换到新创建的分支的话,可以通过 git checkout -b <your-branch-name> 来实现。在 Git 中合并两个分支时会产生一个特殊的提交记录,它有两个父节点。翻译成自然语言相当于:“我要把这两个父节点本身及它们所有的祖先都包含进来。”上图中,HEAD 是指向了 main,我们想将 bugFix 分支的代码合并到 main 分支上来,那么执行 git merge bugFix即可:看到了吗,合并之后多了一个 C4 节点,这个节点的父节点有两个,分别是 C2 与 C3。main 与 HEAD 也指向了合并后的最新节点。如果,我们再切到 bugFix 分支,将 main 里面的代码合并过来,会发生什么呢?还会产生一个新的节点吗?答案是不会产生新的节点,因为此时的 main 是继承至 bugFix 的,所以此时合并啥都不用做,只需要 fast-forward 就好了。所以结果就是,bugFix 直接指向了 main 所指向的 C4 节点。第二种合并分支的方法是 git rebase。Rebase 实际上就是取出一系列的提交记录,“复制”它们,然后在另外一个地方逐个的放下去。Rebase 的优势就是可以创造更线性的提交历史,这听上去有些难以理解。如果只允许使用 Rebase 的话,代码库的提交历史将会变得异常清晰。下面,会举一个例子,然后你可以将最终合并的结果与 merge 出来的结果相比较,就能明白什么叫做线性的提交历史了。这个命令其实是省略了当前 HEAD 指向的分支,完整的因该是 git rebase main bugFix,就是将 bugFix 上的提交 rebase 到 main 上去。rebase 的意思是重新以XX为父节点。上面的图中,C3 的父节点为 C1,rebase 之后的,C3 的父节点就“变成了” C2(一般情况下肯定会有冲突,但是这里就不讨论了),然后将 C3 copy 一份(解决冲突后),放到 C2 的下面,如图:可以看到,C3 节点仍然存在,不过创建了一个 C3’ 作为 C2 的子节点。bugFix 与 HEAD 也指向了“copy” 过来的节点。这样看起来,相比于 merge 是不是更加线性呢!不过有的人喜欢保留合并记录,当然这是属于个人爱好的事情,并无好坏之分。否则的话,也不会出现两种合并方案了。一个小测验,如果,接下来,我们执行 git rebase bugFix main 会发生什么呢?因为,bugFix 是继承至 main,所以只需要 fast-forward 就好了。可以将这情况下的 rebase,看成一种移动 HEAD 的方式。下面的图中,不仅 main 指向了 bugFix,而且 HEAD 还指向了 main。git reset 通过把分支记录回退几个提交记录来实现撤销改动。你可以将这想象成“改写历史”。git reset 向上移动分支,原来指向的提交记录就跟从来没有提交过一样。执行 git reset HEAD~1 后,结果为:Git 把 main 分支移回到 C1;现在我们的本地代码库根本就不知道有 C2 这个提交了。在reset后, C2 所做的变更还在,但是处于未加入暂存区状态。搞清楚什么是暂存区,工作区,请看这个链接:
https://blog.csdn.net/qq_32452623/article/details/78417609
上面的解释感觉并不是特别准确,因为 C2 已经提交了,提交后是无法撤回的,历史是无法改变的。我们再执行 git checkout C2,HEAD 仍然会指向 C2,reset 后,C2 仍然存在,不过可以忽略。你仍然想用它,也可以用。虽然,git log 命令看不到了,但是这个历史确实是存在的。reset 还有一个作用,就是将 add 到暂存区的文件,重新拿出来:虽然在你的本地分支中使用 git reset 很方便,但是这种“改写历史”的方法对大家一起使用的远程分支是无效的哦!为了撤销更改并分享给别人,我们需要使用 git revert。来看演示:当我们执行 git revert HEAD 后,结果为:奇怪!在我们要撤销的提交记录后面居然多了一个新提交!这是因为新提交记录 C2' 引入了更改 —— 这些更改刚好是用来撤销 C2 这个提交的。也就是说 C2' 的状态与 C1 是相同的。revert 之后就可以把你的更改推送到远程仓库与别人分享啦。记住,本地分支用 reset,远程分支用 revert如果你想将一些提交复制到当前所在的位置(HEAD)下面的话, Cherry-pick 是最直接的方式了。我个人非常喜欢 cherry-pick,因为它特别简单。不仅简单,而且强大。看一个例子:执行 git cherry-pick C2 C4,结果为:注意,cherry-pick 后,main 与 HEAD 的位置。这个功能就很强大了,比如说,你开了一个分支,正在做新特性,由于你自己的 commit 风格良好,每做一个逻辑就 commit 一下。在开发的过程中,还顺便优化了一下某个功能,它是一些单独的 commit。在你的新特性开发到一般的时候,有人告知你这个新特性暂时不做了,虽然你很蛋疼,但是作为一个敬业的开发者,你还是想将优化的 commit 合到 main 上去,这个时候就可以用 cherry-pick 了(虽然也有别的命令可以做到)。这个命令与 git rebase 一样,但是后面跟了一个参数 -i,意思是,它可以让用户来干涉(删掉,保留,编辑)每个 commit 被 rebase 的过程,所以,它可以做很多事。具体的还是自己体验一下游戏吧,这个用图说不清楚。这个命令比较常用,而且比较实用,就是用来修改当前提交的。看下面的例子:有时候会发生这样的事情,当我们提交完C2之后,发现提交错了,想修改一下C2重新提交,该怎么做呢?使用 git commit --amend,看执行结果:可以看出,这个使用 reset 也可以做到,不过会麻烦很多。分支很容易被人为移动,并且当有新的提交时,它也会移动。分支很容易被改变,大部分分支还只是临时的,并且还一直在变。你可能会问了:有没有什么可以永远指向某个提交记录的标识呢,比如软件发布新的大版本,或者是修正一些重要的 Bug 或是增加了某些新特性,有没有比分支更好的可以永远指向这些提交的方法呢?当然有了!Git 的 tag 就是干这个用的啊,它们可以(在某种程度上 —— 因为标签可以被删除后重新在另外一个位置创建同名的标签)永久地将某个特定的提交命名为里程碑,然后就可以像分支一样引用了。更难得的是,它们并不会随着新的提交而移动。你也不能检出到某个标签上面进行修改提交,它就像是提交树上的一个锚点,标识了某个特定的位置。当我们执行 git tag v1 C1后,结果如下:这里我们建立了一个标签,指向提交记录 C1,表示这是我们 1.0 版本。我们将这个标签命名为 v1,并且明确地让它指向提交记录 C1,如果你不指定提交记录,Git 会用 HEAD 所指向的位置。以后我们想使用 v1 版本的代码,就直接 checkout v1 就好了。但是需要注意,checkout 后,HEAD 指向的是 C1,而不是 v1,因为 v1 不可变。由于标签在代码库中起着“锚点”的作用,Git 还为此专门设计了一个命令用来描述离你最近的锚点(也就是标签),它就是 git describe!Git Describe 能帮你在提交历史中移动了多次以后找到方向;<ref> 可以是任何能被 Git 识别成提交记录的引用,如果你没有指定的话,Git 会以你目前所检出的位置(HEAD)。<tag>_<numCommits>_g<hash>
tag 表示的是离 ref 最近的标签, numCommits 是表示这个 ref 与 tag 相差有多少个提交记录, hash 表示的是你所给定的 ref 所表示的提交记录哈希值的前几位。当 ref 提交记录上有某个标签时,则只输出标签名称。git describe main 会输出:
v1_2_gC2
git describe side 会输出:
v2_1_gC4
git push <remote> <place>
切到本地仓库中的“main”分支,获取所有的提交,再到远程仓库“origin”中找到“main”分支,将远程仓库中没有的提交记录都添加上去,搞定之后告诉我。git checkout C0
git push origin main
可以看到,push 之后本地与远程的 mian 分支就同步了,而且 o/main 也改变了。其实git push origin main 这个命令其实是省略了一部分,完整的应该是这样:git push origin main:main。我们看一个例子:执行命令 git push origin foo^:main ,结果为:使用这个命令,我们还可以将 foo 分支的代码推送到 main 分支里面,反正很强大就是了,但是一般不这么干。git fetch 完成了仅有的但是很重要的两步:从远程仓库下载本地仓库中缺失的提交记录
更新远程分支指针(如 o/main)
git fetch 实际上将本地仓库中的远程分支更新成了远程仓库相应分支最新的状态。左边是我们的本地仓库,右边是远程仓库。可以看到远程仓库与我们自己的本地仓库多了2个提交,说明有人往远程仓库push了代码,我们需要将这些提交同步到我们的本地仓库,然后再进行开发,看别人改动了什么。那么如何进行同步呢?使用 git fetch 命令,看看执行命令后的效果:可以看到,同步后,o/main已经与远程仓库完全一致了。o/main是远程仓库的指针,所以它一直指向上次与远程仓库通信时的位置。需要注意,我们本地的 main还是指向的原来的位置,有需要可自己移动。git fetch 并不会改变你本地仓库的状态。它不会更新你的 main 分支,也不会修改你磁盘上的文件。理解这一点很重要,因为许多开发人员误以为执行了 git fetch 以后,他们本地仓库就与远程仓库同步了。它可能已经将进行这一操作所需的所有数据都下载了下来,但是并没有修改你本地的文件。注意,上图中,远程的最新提交是 c3,本地的最新提交是 C2。如果,我们执行了 git fetch 命令,那么就会出现分叉。所以,还要进行 merge 操作:git fecth
git merge o/main
嗯,这个图没画好,不过我也是用的别人做的图,将就着看吧。现在,本地的 o/main 指向 c3,main*指向 c4,由于合并操作,c4 是合并产生的,它的父节点是 c2 与 c3。其实呢,git pull 就是做了上面的操作,不过他将两个命令合成了一个。Git 有两种关于 <source> 的用法是比较诡异的,即你可以在 git push 或 git fetch 时不指定任何 source,方法就是仅保留冒号和 destination 部分,source 部分留空。git push origin :side
git fetch origin :bugFix
如果 push 空 到远程仓库会如何呢?它会删除远程仓库中的分支!执行 git push origin :foo,结果为:如果 fetch 空 到本地,会在本地创建一个新分支。看下图:执行 git fetch origin :bar,结果为: